//Source file: D:\\WTK104\\apps\\Toddtris\\src\\com\\toddsoft\\toddtris\\model\\Well.java

package com.toddsoft.toddtris.model;

import java.util.Random;
import com.toddsoft.util.ByteArrayHandler;

/**
 *
 */
public class Well {
    public static final int WELL_WIDTH = 10;
    public static final int WELL_HEIGHT = 18;
    private static final int HIDDEN_ROWS = 4;
    private static final int X_START = 3;
    //private static final int Y_START = 0;

    /**
     * one copy of each block that can used to identify the next block
     */
    private static Block nextBlockList[];
//    private static final Block nextBlockList[];

    /**
     * list of all listeners
     */
    protected WellListenerList listenerList = new WellListenerList ();

    private int deadElems[][] = new int [WELL_WIDTH][WELL_HEIGHT + HIDDEN_ROWS] ;
    private int blockX = X_START;
    private int blockY;// = Y_START;
    private int lines = 0;
    private boolean moving = false;
    private boolean inProgress = false;
    private boolean newlyReset = false;

    /* 
     * Time which can be subtracted from current time to work out how long the
     * well has been running.
     * Initially set to be the current time, but adjusted to compensate for the
     * well being paused.
     */
    private long timeOffset;

    /* The time at which the well was stopped if applicable, -1 if not */
    private long stoppedTime;

    /* The target number of lines */
    private int targetLines;

    /* The height of the randomly placed blocks when the well is reset */
    private int startHeight;
                                           
    private int level;

    private int nextBlockIndex;
    private Block block;
    
    /*
     * One copy of each block per Well instance, one of these blocks is
     * used to represent the current block.
     * Not static as these can be rotated.
     */
//    private final Block blockList[];
    private Block[] blockList;

    private Random blockIndexRandom = new Random () {
            /**
             * @return 0 to Block.NUM_BLOCKS - 1
             */
            public int nextInt() {
                return(((super.nextInt() % Block.NUM_BLOCKS) 
                                         + Block.NUM_BLOCKS) 
                                         % Block.NUM_BLOCKS);//prevent negatives
            } };
    
    private Random startHeightRandom = new Random () {
            /**
             * @return 0 to Block.NUM_BLOCKS - 1
             */
            public int nextInt() {
                return (super.nextInt() % 3);
            } };


    /* static initializer */
    static {

        /* create the list of possible next blocks */
        nextBlockList = new Block[Block.NUM_BLOCKS];
        for (int i = 0; i < Block.NUM_BLOCKS; i++) {
            nextBlockList[i] = new Block(i);
        }     
    }

    /* instance initializer */
    {
        /* create the list of possible blocks */
        blockList = new Block[Block.NUM_BLOCKS];
        for (int i = 0; i < Block.NUM_BLOCKS; i++) {
            blockList[i] = new Block(i);
        }
        /* start the thread */
        (new WellRunner()).start();     
    }

    public Well(int targetLines, int startHeight, int level) {

        this.targetLines = targetLines;
        this.level = level;
        setStartHeight(startHeight);

        /* reset the well */
        reset();     
    }

    /**
     * @param bytes
     */
    public Well(byte[] bytes) {

        ByteArrayHandler bah = new ByteArrayHandler(bytes);
        /* recreate deadElems */
        for (int y = 0; y < WELL_HEIGHT + HIDDEN_ROWS; y++) {
            for (int x = 0; x < WELL_WIDTH; x++) {
                this.deadElems[x][y] = bah.getByte();
            }
        }     

        this.setBlock(bah.getByte());
        int rotationIndex = bah.getByte();
        for (int i = 0; i < rotationIndex; i++) {
            this.block.rotate();
        }
        this.nextBlockIndex = bah.getByte();
        this.blockX = bah.getByte();
        this.blockY = bah.getByte();
        this.moving = bah.getBoolean();
        this.inProgress = bah.getBoolean();
        this.lines = bah.getInt();
        this.targetLines = bah.getInt();
        this.setStartHeight(bah.getInt());
        this.level = bah.getInt();

        long runningTimeMillis = bah.getLong();

        /* set time offset to be now minus the well running time */
        this.timeOffset = System.currentTimeMillis() - runningTimeMillis;

        /* set stopped time to now if well not running */
        this.stoppedTime = this.moving ? -1 : System.currentTimeMillis();

    }
    
    /**
     * @return byte[]
     */
    public byte[] toByteArray() {

        ByteArrayHandler bah = new ByteArrayHandler();

        //write the dead elements row by row
        for (int y = 0; y < WELL_HEIGHT + HIDDEN_ROWS; y++) {
            for (int x = 0; x < WELL_WIDTH; x++) {
                bah.addByte((byte)this.deadElems[x][y]);
            }
        }
        bah.addByte((byte)block.getBlockNum());
        bah.addByte((byte)block.getRotationIndex());
        bah.addByte((byte)this.nextBlockIndex);
        bah.addByte((byte)this.blockX);
        bah.addByte((byte)this.blockY);
        bah.addBoolean(this.moving);
        bah.addBoolean(this.inProgress);
        bah.addInt(this.lines);
        bah.addInt(this.targetLines);
        bah.addInt(this.startHeight);
        bah.addInt(this.level);
        bah.addLong(this.getRunningTimeInMillis());

        return bah.getBytes();     
    }

    public void updateChallengeSettings(int targetLines, 
                                        int startHeight,
                                        int level) {
        this.targetLines = targetLines;
        this.setStartHeight(startHeight);
        this.level = level;
    }

    public int getTargetLines() {
        return this.targetLines;
    }
    
    public int getLevel() {
        return this.level;
    }
    
    private void setStartHeight(int sh) {
        if (sh < 0) {
            sh = 0;
        }
        if (sh > WELL_HEIGHT) {
            sh = WELL_HEIGHT;
        }
        this.startHeight = sh;
    }
    
    public void reset() {
        for (int i = 0; i < WELL_WIDTH; i++) {
            for (int j = 0; j < WELL_HEIGHT + HIDDEN_ROWS; j++) {
                deadElems[i][j] = 0;
            }
        }

        /* choose indices for the current and next block */
        nextBlockIndex = blockIndexRandom.nextInt();
        newBlock();

        //blockX = X_START;
        //blockY = Y_START;

        lines = 0;

        moving = false;

        inProgress = false;

        newlyReset = true;

        /* 
         * Create the random blocks at the bottom of the well.
         * Set one in three spaces in these rows to be contain blocks.
         */
        for (int i = 1; i <= this.startHeight; i++) {
            for (int j = 0; j < WELL_WIDTH; j++) {
                if (this.startHeightRandom.nextInt() == 0) {
                    deadElems[j][WELL_HEIGHT + HIDDEN_ROWS - i] = -8;
                }
            }
        }

        this.timeOffset = System.currentTimeMillis();
        this.stoppedTime = -1;

        listenerList.wellReset(this);  
    }
    
    /**
     * @param listener
     */
    public void addListener(WellListener listener) {
        listenerList.addListener(listener);     
    }
    
    /**
     * Can't start well not in progress
     */
    public void startWell() {
        if (newlyReset || (this.inProgress && !this.moving)) {
            this.moving = true;
            this.inProgress = true;
            this.newlyReset = false;

            if (this.stoppedTime != -1) {
                /* 
                 * add the amount of time the well was stopped for onto the 
                 * time offset
                 */
                this.timeOffset += System.currentTimeMillis() 
                                   - this.stoppedTime;
                this.stoppedTime = -1;
            }
        }     
    }
    
    public void stopWell() {
        if (this.moving) {
            this.moving = false;
            this.stoppedTime = System.currentTimeMillis();
        }
    }

    /**
     * @return The amount of time the well has been running for.
     *         This does not include any time when the well was stopped.
     */
    public long getRunningTimeInMillis() {
        if (this.moving) {
            return System.currentTimeMillis() - this.timeOffset;
        } else {
            if (this.stoppedTime == -1) {
                /* not moving and never stopped, can't have started */
                return 0;
            } else {
                return this.stoppedTime - this.timeOffset;
            }
        }
    }
    
    /**
     * @return boolean
     */
    public boolean isComplete() {
        return ((this.targetLines > 0) 
                && (this.getLines() >= this.targetLines));
    }    

    /**
     * @return boolean
     */
    public boolean isMoving() {
        return moving;     
    }
    
    /**
     * @return boolean
     */
    public boolean isInProgress() {
        return this.inProgress;     
    }
    
    /**
     * @return boolean
     */
    public boolean isGameOver() {
        return (!this.isComplete() && !this.newlyReset && !this.inProgress);     
    }
    
    /**
     * Gets the element in the position indicated ignoring the hidden lines
     * @param viewX
     * @param viewY
     * @return int
     */
    public int getElem(int viewX, int viewY) {
        
        int x = viewX;
        int y = viewY + HIDDEN_ROWS;

        /*
         * copy the members into temporary variable to avoid 
         * problems, in particular ArrayIndexOutOfBoundsException
         */
        int tempBlockX = blockX;
        int tempBlockY = blockY;
        Block tempBlock = block;

        int blockSize = tempBlock.getSize();
        if ((x >= tempBlockX) && (y >= tempBlockY)
                && (x < (tempBlockX + blockSize)) && (y < (tempBlockY + blockSize))) {

            /* This is within the blockArea */
            int blockElem = tempBlock.getElem(x - tempBlockX, y - tempBlockY);

            /*
             * if this block element is greater than zero then return this
             * number, otherwise just let this fall
             * fall through to return the space in this position of deadElems
             */
            if (blockElem > 0) {
                return blockElem;
            }
        }
        return deadElems[x][y];     
    }
    
    /**
     * @return int
     */
    public int getLines() {
        return lines;     
    }

    /**
     * Only makes sense when a target has been set.
     * @return the number of remaining lines, or simply 0 if no target is set
     */
    public int getRemainingLines() {
        return (this.getTargetLines() == 0)
               ? 0                              
               : this.isComplete()             
                       ? 0                     //don't give a -ve
                       : this.getTargetLines() 
                           - this.getLines();  //remaining lines
    }
    
    /**
     * @return com.toddsoft.toddtris.model.Block
     */
    public Block getNextBlock() {
        return nextBlockList[nextBlockIndex];     
    }

    public int getStartHeight() {
        return this.startHeight;
    }
    
    /**
     * @return boolean
     */
    public synchronized boolean left() {
        if (checkEmpty(block, blockX - 1, blockY)) {
            blockX--;
            listenerList.wellChanged(this);
            return true;
        } else {
            return false;
        }     
    }
    
    /**
     * @return boolean
     */
    public synchronized boolean right() {
        if (checkEmpty(block, blockX + 1, blockY)) {
            blockX++;
            listenerList.wellChanged(this);
            return true;
        } else {
            return false;
        }     
    }
    
    /**
     * @return boolean
     */
    public synchronized boolean rotate() {
        block.rotate();
        if (checkEmpty(block, blockX, blockY)) {
            listenerList.wellChanged(this);
            return true;
        } else {
            block.antirotate();
            return false;
        }     
    }
    
    /**
     * @return boolean
     */
    public synchronized boolean antirotate() {
        block.antirotate();
        if (checkEmpty(block, blockX, blockY)) {
            listenerList.wellChanged(this);
            return true;
        } else {
            block.rotate();
            return false;
        }     
    }
    
    /**
     * synchronize this otherwise two downs can occur 
     * and the next block is wrong
     * @return true if not game over
     */
    public synchronized boolean down() {
        if (blockDown()) {
            listenerList.wellChanged(this);
            return true;
        } else {
            return landBlock();
        }     
    }
    
    /**
     * @return boolean
     */
    public synchronized boolean drop() {
        while (blockDown()) {
            // do nothing
        }

        return landBlock();     
    }
    
    /**
     * @return boolean
     */
    private boolean blockDown() {
        if (checkEmpty(block, blockX, blockY + 1)) {
            blockY++;
            return true;
        } else {
            return false;
        }     
    }
    
    /**
     * @return whether this is game over
     */
    private boolean landBlock() {
        putDeadBlock();

        /* try and get a new block, returns false if there is not room (ie game over) */
        if (newBlock()) {

            int linesMade = checkLines();
            incLines(linesMade);

            listenerList.madeLines(this, linesMade);
            listenerList.wellChanged(this);
            listenerList.blockLanded(this);
            return true;

        } else {

            /* game over */
            this.inProgress = false;
            this.stopWell();
            listenerList.wellChanged(this);
            listenerList.gameOver(this);
            listenerList.blockLanded(this);
            return false;
        }     
    }
    
    /**
     * @param b
     * @param x
     * @param y
     * @return boolean
     */
    private boolean checkEmpty(Block b, int x, int y) {
        try {
            for (int i = 0; i < b.getSize(); i++) {
                for (int j = 0; j < b.getSize(); j++) {
                    if ((b.getElem(i, j) != 0) 
                            && (deadElems[x+i][y+j] < 0)) {

                        return false;
                    }
                }
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            return false;
        }
        return true;     
    }
    
    private void putDeadBlock() {
        for(int i = 0; i < block.getSize(); i++) {
            for (int j = 0; j < block.getSize(); j++) {
                if (block.getElem(i,j) != 0) {

                    deadElems[blockX + i][blockY + j] 
                            = 0 - block.getElem(i, j);
                }
            }
        }     
    }
    
    /**
     * @param l
     */
    private void removeLine(int l) {
        for (int j = l; j > 0; j--) {
            for (int i = 0; i < WELL_WIDTH; i++) {
                deadElems[i][j] = deadElems[i][j-1];
            }
        }
        for (int i = 0; i < WELL_WIDTH; i++) {
            deadElems[i][0] = 0;
        }     
    }
    
    /**
     * @param l
     * @return boolean
     */
    private boolean lineFull(int l) {
        for (int i = 0; i < WELL_WIDTH; i++) {
            if (deadElems[i][l] == 0) {

                return false;
            }
        }
        return true;     
    }
    
    /**
     * Get the height of the stack. This is the height of the
     * highest block.
     * @return height of highest block as a number of rows.
     */
    public int getStackHeight() {
        int j;
        for (j = 0; j < WELL_HEIGHT; j++) {
            for (int i = 0; i < WELL_WIDTH; i++) {
                if (deadElems[i][j + HIDDEN_ROWS] < 0) {
                    return j;
                }
            }
        }
        return WELL_HEIGHT - j;     
    }
    
    /**
     * @return int
     */
    private int checkLines() {
        int linesMade = 0;
        for (int j = 0; j < WELL_HEIGHT + HIDDEN_ROWS; j++) {
            if (lineFull(j)) {
                removeLine(j);
                linesMade++;
            }
        }
        return linesMade;     
    }
    
    /**
     * @param l
     */
    protected void incLines(int l) {
        lines += l;     

        /* see if the challenge has been completed */
        if (this.isComplete()) {
            this.inProgress = false;
            this.stopWell();
            listenerList.wellChanged(this);
            listenerList.complete(this);
        }
    }
    
    /**
     * @return boolean
     */
    private boolean newBlock() {
        boolean ret = setBlock(nextBlockIndex);
        nextBlockIndex = blockIndexRandom.nextInt();
        return ret;     
    }
    
    /**
     * @param blockNum
     * @return boolean
     */
    private boolean setBlock(int blockNum) {
        /* reset the current block */
        if (block != null) {
            block.reset();
        }

        /* assign the new block */
        block = blockList[blockNum];
        blockX = X_START;
        blockY = 4 - this.block.getSize();
        return checkEmpty(block, blockX, blockY);     
    }
    
    /**
     * @param blankCol
     */
    private void addLine(int blankCol) {
        for (int j = 1; j < WELL_HEIGHT; j++) {
            for (int i = 0; i < WELL_WIDTH; i++) {
                deadElems[i][j - 1] = deadElems[i][j];

            }
        }
        for (int i = 0; i < WELL_WIDTH; i++) {
            deadElems[i][WELL_HEIGHT - 1] = -8;
        }
        deadElems[blankCol][WELL_HEIGHT - 1] = 0;     
    }
    
    private void updateListener() {
        listenerList.wellChanged(this);     
    }
    
    private class WellRunner extends Thread {
        
        public void run() {
    
            int sleepTime;
            while(true) {
                if (moving) {
                    down();
                    updateListener();
                    try {
                        sleepTime=450-((int) (((getLines() + (level * 10)) * 5) / 2));
                        sleepTime = (sleepTime > 10 ) ? sleepTime : 10;
                        Thread.sleep(sleepTime);
                    } catch (InterruptedException ie){
                    }
                } else {
    
                    /* need to sleep, otherwise kills the client */
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException ie){
                    }
                }
            }         
        }
    }
}
